import { Body, Box, Cylinder, Quaternion, RaycastVehicle, Vec3 } from "cannon-es";
import { Box3, Group, Vector3 } from "three";

const options = {
  chassis: {
    width: 0,
    height: 0,
    length: 0,
    mass: 250
  },
  wheel: {
    radius: .27,
    width: .2,
    height: 0,
    offsetWidth: 0.55,
    frontOffsetLength: 0.8,
    backOffsetLength: 0.7,
    suspensionStiffness: 55,
    suspensionRestLength: 0.5,
    frictionSlip: 30,
    dampingRelaxation: 2.3,
    dampingCompression: 4.3,
    maxSuspensionForce: 10000,
    rollInfluence: 0.01,
    maxSuspensionTravel: 1,
    customSlidingRotationalSpeed: 30,
    useCustomSlidingRotationalSpeed: true,
    directionLocal: new Vec3(0, 0, -1),
    axleLocal: new Vec3(1, 0, 0),
    chassisConnectionPointLocal: new Vec3(0, 0, 0)
  },
  control: {
    maxSteerVal: 0.5,
    maxForce: 380,
    brakeForce: 20
  }
};

export default class Car {
  constructor(physics) {
    this.physics = physics;
    this.options = options;
    this.main = new Group();
    this.physicsBodies = {
      chassis: null,
      wheels: []
    };
  }

  build(model) {
    this.model = model;
    this.body = this.createBody();
    this.wheels = this.createWheels();
    this.vehicle = this.createVehicle();

    this.main.add(this.body);
    this.main.add(this.wheels);
    this.setControls();
  }

  createBody() {
    const body = new Group();
    this.model.children.forEach((item) => {
      if (!item.name.includes("wheel")) {
        body.add(item.clone());
      }
    });

    const wheelBU = this.createWheel("left");
    wheelBU.name = "wheelBU";
    wheelBU.position.x = 0.1;
    wheelBU.position.z = -0.402342;
    wheelBU.position.y = 0.742586;
    wheelBU.rotation.z = Math.PI / 2;
    body.add(wheelBU);
    const box = new Box3().setFromObject(body);
    const size = {
      x: box.max.x - box.min.x,
      y: box.max.y - box.min.y,
      z: box.max.z - box.min.z
    };
    const center = {
      x: (box.max.x + box.min.x) / 2,
      y: (box.max.y + box.min.y) / 2,
      z: (box.max.z + box.min.z) / 2
    };

    body.children.forEach(e => {
      e.position.x -= center.x;
      e.position.y -= center.y;
      e.position.z -= center.z;
    });

    this.options.chassis.width = size.x / 2;
    this.options.chassis.length = size.y / 2;
    this.options.chassis.height = size.z / 2;

    return body;
  }

  createWheels() {
    const wheels = new Group();
    const wheelL = this.createWheel("left");
    const wheelR = this.createWheel("right");

    const wheelFL = wheelL.clone();
    wheelFL.name = "wheelFL";
    wheels.add(wheelFL);

    const wheelFR = wheelR.clone();
    wheelFR.name = "wheelFR";
    wheels.add(wheelFR);

    const wheelBL = wheelL.clone();
    wheelBL.name = "wheelBL";
    wheels.add(wheelBL);

    const wheelBR = wheelR.clone();
    wheelBR.name = "wheelBR";
    wheels.add(wheelBR);

    return wheels;
  }

  createWheel(direction = "left") {
    const wheel = new Group();
    this.model.children.forEach(item => {
      if (item.name.includes("wheel")) {
        const mesh = item.clone();
        mesh.position.set(0, 0, 0);
        if (direction === "right") {
          mesh.rotateZ(Math.PI);
        }
        wheel.add(mesh);
      }
    });
    return wheel;
  }

  createVehicle() {
    const chassisShape = new Box(new Vec3(this.options.chassis.width, this.options.chassis.length, this.options.chassis.height));
    const chassisBody = new Body({
      mass: this.options.chassis.mass,
      allowSleep: true,
      shape: chassisShape
    });
    chassisBody.position.set(26.7, -16.8, 10);
    chassisBody.angularVelocity.set(0, 0, .5);
    this.physicsBodies.chassis = chassisBody;

    const vehicle = new RaycastVehicle({
      chassisBody,
      indexRightAxis: 0,
      indexUpAxis: 2,
      indexForwardAxis: 1
    });

    for (let i = 0; i < this.wheels.children.length; i++) {
      const option = { ...this.options.wheel };
      switch (i) {
        case 0:
          option.chassisConnectionPointLocal.set(option.offsetWidth, -option.frontOffsetLength, 0);
          break;
        case 1:
          option.chassisConnectionPointLocal.set(-option.offsetWidth, -option.frontOffsetLength, 0);
          break;
        case 2:
          option.chassisConnectionPointLocal.set(option.offsetWidth, option.backOffsetLength, 0);
          break;
        case 3:
          option.chassisConnectionPointLocal.set(-option.offsetWidth, option.backOffsetLength, 0);
          break;
      }
      vehicle.addWheel(option);

      const radius = option.radius;
      const width = option.width;
      const wheelShape = new Cylinder(radius, radius, width, 30);
      const wheelBody = new Body({ mass: 20, material: this.physics.materials.wheel });

      const wheelQuaternion = new Quaternion();
      wheelQuaternion.setFromAxisAngle(new Vec3(0, 0, 1), Math.PI / 2);
      wheelBody.addShape(wheelShape, new Vec3(), wheelQuaternion);

      wheelBody.type = Body.KINEMATIC;
      wheelBody.collisionFilterGroup = 0;

      this.physicsBodies.wheels[i] = wheelBody;
      this.physics.world.addBody(wheelBody);
    }

    vehicle.addToWorld(this.physics.world);

    return vehicle;
  }

  update() {
    const physicsChassis = this.physicsBodies.chassis;
    this.body.position.copy(physicsChassis.position);
    this.body.quaternion.copy(physicsChassis.quaternion);

    for  (let i = 0; i < this.vehicle.wheelInfos.length; i++) {
      this.vehicle.updateWheelTransform(i)
      const wheelInfo = this.vehicle.wheelInfos[i]
      const transform = wheelInfo.worldTransform
      const wheelBody = this.physicsBodies.wheels[i]
      wheelBody.position.copy(transform.position)
      wheelBody.quaternion.copy(transform.quaternion)

      const wheel = this.wheels.children[i]
      wheel.position.copy(wheelBody.position)
      wheel.quaternion.set(-transform.quaternion.x, -transform.quaternion.y, transform.quaternion.z, transform.quaternion.w)
    }
  }

  setControls() {
    const maxSteerVal = this.options.control.maxSteerVal;
    const maxForce = this.options.control.maxForce;
    const brakeForce = this.options.control.brakeForce;

    this.controls = (event) => {
      const up = (event.type === "keyup");
      if (!up && event.type !== "keydown") {
        return;
      }
      this.vehicle.setBrake(0, 0);
      this.vehicle.setBrake(0, 1);
      this.vehicle.setBrake(0, 2);
      this.vehicle.setBrake(0, 3);

      switch (event.key) {
        case "w":
        case "ArrowUp":
          this.vehicle.applyEngineForce(up ? 0 : -maxForce, 2);
          this.vehicle.applyEngineForce(up ? 0 : -maxForce, 3);
          break;
        case "s":
        case "ArrowDown":
          this.vehicle.applyEngineForce(up ? 0 : maxForce, 2);
          this.vehicle.applyEngineForce(up ? 0 : maxForce, 3);
          break;
        case "a":
        case "ArrowLeft":
          this.vehicle.setSteeringValue(up ? 0 : maxSteerVal, 0);
          this.vehicle.setSteeringValue(up ? 0 : maxSteerVal, 1);
          break;
        case "d":
        case "ArrowRight":
          this.vehicle.setSteeringValue(up ? 0 : -maxSteerVal, 0);
          this.vehicle.setSteeringValue(up ? 0 : -maxSteerVal, 1);
          break;
        case "b":
          this.vehicle.setBrake(brakeForce, 0);
          this.vehicle.setBrake(brakeForce, 1);
          this.vehicle.setBrake(brakeForce, 2);
          this.vehicle.setBrake(brakeForce, 3);
          break;
      }
    };

    window.addEventListener("keydown", this.controls);
    window.addEventListener("keyup", this.controls);
  }
}